import importlib.util
import os
import sys
from typing import Dict, Any, List, Tuple
import logging
import hmac
import hashlib
import time
import urllib.parse
import requests
import base64
from datetime import datetime
import traceback

# 配置日志
logger = logging.getLogger('TaskEngine')
logger.setLevel(logging.DEBUG)

class SafeSandbox:
    """安全沙箱类，用于隔离执行用户脚本"""
    def __init__(self):
        self.safe_builtins = {}
        self._setup_safe_builtins()

    def _setup_safe_builtins(self):
        """设置安全的内置函数"""
        safe_list = ['abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable',
                     'chr', 'classmethod', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod',
                     'enumerate', 'eval', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals',
                     'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass',
                     'iter', 'len', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object',
                     'oct', 'ord', 'pow', 'property', 'range', 'repr', 'reversed', 'round',
                     'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super',
                     'tuple', 'type', 'vars', 'zip']

        for name in safe_list:
            if name in __builtins__:
                self.safe_builtins[name] = __builtins__[name]

    def run_script(self, script_path: str, params: Dict[str, Any]) -> Any:
        """
        在安全沙箱中运行脚本
        :param script_path: 脚本文件路径
        :param params: 参数字典
        :return: 脚本执行结果
        """
        if not os.path.exists(script_path):
            logger.error(f"脚本文件不存在: {script_path}")
            return {"error": "脚本文件不存在"}
            
        # 确保项目根目录在Python路径中，以便导入user_parameters等模块
        project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        if project_root not in sys.path:
            sys.path.insert(0, project_root)
            logger.info(f"已添加项目根目录到Python路径: {project_root}")

        try:
            # 创建独立的命名空间
            namespace = {
                '__builtins__': self.safe_builtins,
                '__file__': script_path,
                '__name__': 'user_script',
            }

            # 确保项目根目录在Python路径中，以便导入user_parameters等模块
            project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
            if project_root not in sys.path:
                sys.path.insert(0, project_root)
                logger.info(f"已添加项目根目录到Python路径: {project_root}")
            
            # 动态加载模块
            spec = importlib.util.spec_from_file_location('user_script', script_path)
            if spec is None or spec.loader is None:
                logger.error(f"无法加载脚本: {script_path}")
                return {"error": "无法加载脚本"}

            module = importlib.util.module_from_spec(spec)
            sys.modules['user_script'] = module
            spec.loader.exec_module(module)

            # 检查是否存在 main 函数
            if hasattr(module, 'main'):
                messages = module.main(params)
                logger.info(f"脚本返回值类型: {type(messages)}, 值: {messages}")
                if isinstance(messages, list):
                    return messages
                else:
                    logger.error("main应返回字符串列表")
                    return {"error": "main返回值类型错误"}
            else:
                logger.error("脚本中未找到main函数")
                return {"error": "脚本中未找到main函数"}
        except Exception as e:
            error_trace = traceback.format_exc()
            logger.error(f"执行脚本时出错: {str(e)}\n{error_trace}")
            return {"error": str(e), "traceback": error_trace}

class TaskEngine:
    """定时任务执行引擎"""
    def __init__(self):
        self.sandbox = SafeSandbox()
        # 重试配置
        self.max_retries = 3  # 最大重试次数
        self.retry_delay = 5  # 重试间隔（秒）

    def execute_task(self, script_path: str, params: Dict[str, Any], robot_webhook: str = None, robot_secret: str = None, task_id: int = None, at_persons: str = '', retry_count: int = 0) -> bool:
        """
        执行定时任务并发送消息到钉钉机器人
        :param script_path: 脚本文件路径
        :param params: 参数字典
        :param robot_webhook: 钉钉机器人webhook地址
        :param robot_secret: 钉钉机器人签名密钥
        :param task_id: 任务ID，用于精确查找任务
        :param at_persons: @人的手机号列表，多个手机号用逗号分隔
        :param retry_count: 当前重试次数
        :return: 是否执行成功
        """
        def send_dingtalk_message(message: str) -> Tuple[bool, str]:
            """发送钉钉消息"""
            if not robot_webhook:
                return False, "未配置钉钉机器人webhook"
                
            try:
                timestamp = str(round(time.time() * 1000))
                secret_enc = robot_secret.encode('utf-8')
                string_to_sign = f"{timestamp}\n{robot_secret}"
                string_to_sign_enc = string_to_sign.encode('utf-8')
                hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
                sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
                
                url = f"{robot_webhook}&timestamp={timestamp}&sign={sign}"
                headers = {'Content-Type': 'application/json'}
                
                # 构建消息数据
                data = {
                    "msgtype": "text",
                    "text": {"content": message}
                }
                
                # 如果有@人配置，添加@功能
                if at_persons and at_persons.strip():
                    # 解析手机号列表
                    phone_numbers = [phone.strip() for phone in at_persons.split(',') if phone.strip()]
                    if phone_numbers:
                        data["at"] = {
                            "atMobiles": phone_numbers,
                            "isAtAll": False
                        }
                        logger.info(f"添加@人配置: {phone_numbers}")
                
                response = requests.post(url, headers=headers, json=data, timeout=10)
                if response.status_code != 200 or response.json().get('errcode') != 0:
                    error_msg = f"钉钉API返回错误: {response.text}"
                    logger.error(error_msg)
                    return False, error_msg
                return True, ""
            except Exception as e:
                error_msg = f"钉钉消息发送异常: {str(e)}"
                logger.error(error_msg)
                return False, error_msg
                
        try:
            # 使用executor service的数据库配置
            import os
            import sys
            sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
            from models import ScheduledTask, TaskExecutionLog
            from services.executor_service import app, db
            
            # 优先使用task_id查找任务，如果没有则使用script_path
            if task_id:
                with app.app_context():
                    task = ScheduledTask.query.get(task_id)
            else:
                with app.app_context():
                    task = ScheduledTask.query.filter_by(script_path=script_path).first()
                
            if not task:
                logger.error(f"找不到任务: script_path={script_path}, task_id={task_id}")
                return False
        except Exception as e:
            logger.error(f"数据库查询错误: {str(e)}")
            return False
                
        start_time = datetime.now()
        
        # 重定向标准输出到日志
        import io
        old_stdout = sys.stdout
        string_io = io.StringIO()
        sys.stdout = string_io
        
        error_occurred = False
        error_info = ""
        
        try:
            messages = self.sandbox.run_script(script_path, params)
        except Exception as e:
            error_occurred = True
            error_info = str(e)
            traceback.print_exc()
            messages = None
            
            # 捕获所有脚本错误并记录到控制台输出
            error_trace = traceback.format_exc()
            logger.error(f"脚本执行错误: {str(e)}\n{error_trace}")
            
            # 重试逻辑
            if retry_count < self.max_retries:
                logger.info(f"任务执行失败，准备第{retry_count + 1}次重试，等待{self.retry_delay}秒...")
                time.sleep(self.retry_delay)
                return self.execute_task(
                    script_path, params, robot_webhook, robot_secret,
                    task_id, at_persons, retry_count + 1
                )
        
        # 获取控制台输出和错误信息
        try:
            console_output = string_io.getvalue()
        except AttributeError:
            # 如果getvalue()方法不存在，尝试其他方式获取输出
            console_output = ""
        finally:
            sys.stdout = old_stdout
        
        end_time = datetime.now()
        duration = (end_time - start_time).total_seconds()
        
        # 日志内容优化
        log_type = 'script_execution'
        script_success = False
        log_content = ""
        # 1. run_script 返回异常 dict
        if isinstance(messages, dict) and 'error' in messages:
            error_occurred = True
            error_info = messages.get('error', '')
            error_trace = messages.get('traceback', '')
            log_content = f"脚本执行异常: {error_info}\n{error_trace}\n{console_output}"
        # 2. main 返回 None 或空列表 - 修改为成功状态
        elif messages is None or (isinstance(messages, list) and len(messages) == 0):
            error_occurred = False  # 改为False，表示脚本执行成功
            script_success = True   # 改为True，表示脚本执行成功
            log_content = f"脚本执行完成，无消息返回\n{console_output}"
        # 3. 正常返回
        elif isinstance(messages, list):
            script_success = True
            log_content = console_output if console_output else "脚本执行完成"
        else:
            error_occurred = True
            error_info = "main 返回值类型错误"
            log_content = f"main 返回值类型错误\n{console_output}"
            
        # 检查钉钉消息发送状态
        dingtalk_failure = False
        dingtalk_error_messages = []
        # 只有当脚本成功且有消息返回时才发送钉钉消息
        if script_success and robot_webhook and messages and isinstance(messages, list) and len(messages) > 0:
            for i, msg in enumerate(messages):
                if isinstance(msg, str):
                    success, error_msg = send_dingtalk_message(msg)
                    if not success:
                        dingtalk_failure = True
                        dingtalk_error_messages.append(f"第{i+1}条消息发送失败: {error_msg}")
                        log_content += f"\n钉钉消息发送失败: {error_msg}"
            
        # 如果有钉钉发送失败，添加到日志内容
        if dingtalk_error_messages:
            log_content += f"\n\n钉钉消息发送失败详情:\n" + "\n".join(dingtalk_error_messages)

        # 整体成功判断：脚本执行成功，且（无钉钉配置 或 钉钉消息发送成功）
        overall_success = script_success and not error_occurred and (not robot_webhook or not dingtalk_failure)

        # 再次验证任务是否存在（防止任务在执行过程中被删除）
        if task and task.id:
            try:
                with app.app_context():
                    # 创建脚本执行日志记录
                    script_execution_log = TaskExecutionLog(
                        task_id=task.id,
                        start_time=start_time,
                        end_time=end_time,
                        duration=duration,
                        status='成功' if overall_success else '失败',
                        log_content=log_content,
                        log_type='script_execution'
                    )
                    db.session.add(script_execution_log)
                    
                    # 如果有钉钉消息发送，创建钉钉消息发送日志记录
                    if messages and isinstance(messages, list) and len(messages) > 0 and robot_webhook:
                        dingtalk_log_content = f"钉钉消息发送详情:\n"
                        for i, msg in enumerate(messages, 1):
                            if isinstance(msg, str):
                                dingtalk_log_content += f"第{i}条消息: {msg[:100]}{'...' if len(msg) > 100 else ''}\n"
                        
                        if dingtalk_error_messages:
                            dingtalk_log_content += f"\n发送失败的消息:\n" + "\n".join(dingtalk_error_messages)
                        else:
                            dingtalk_log_content += "\n所有消息发送成功"
                            
                        dingtalk_execution_log = TaskExecutionLog(
                            task_id=task.id,
                            start_time=end_time,  # 钉钉发送在脚本执行之后
                            end_time=end_time,
                            duration=0,  # 钉钉发送时间很短，可以忽略
                            status='成功' if not dingtalk_failure else '失败',
                            log_content=dingtalk_log_content,
                            log_type='dingtalk_log'
                        )
                        db.session.add(dingtalk_execution_log)
                    
                    # 更新任务状态
                    task.last_run_time = end_time
                    task.last_run_duration = duration
                    
                    # 最终状态判断 - 只有脚本执行成功且钉钉消息发送成功才算整体成功
                    task.last_run_status = '成功' if overall_success else '失败'
                    
                    db.session.commit()
            except Exception as e:
                logger.error(f"保存日志记录失败: {str(e)}")
        else:
            logger.warning(f"任务不存在，跳过日志创建: script_path={script_path}, task_id={task_id}")
        return script_success

    def execute_script_only(self, script_path: str, params: Dict[str, Any], task_id: int = None) -> List[str]:
        """
        仅执行脚本，不发送消息
        :param script_path: 脚本文件路径
        :param params: 参数字典
        :param task_id: 任务ID
        :return: 脚本返回的消息列表
        """
        try:
            # 使用executor service的数据库配置
            import os
            import sys
            sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
            from models import ScheduledTask, TaskExecutionLog
            from services.executor_service import app, db
            
            # 优先使用task_id查找任务，如果没有则使用script_path
            if task_id:
                with app.app_context():
                    task = ScheduledTask.query.get(task_id)
            else:
                with app.app_context():
                    task = ScheduledTask.query.filter_by(script_path=script_path).first()
                
            if not task:
                logger.error(f"找不到任务: script_path={script_path}, task_id={task_id}")
                return []
        except Exception as e:
            logger.error(f"数据库查询错误: {str(e)}")
            return []
                
        start_time = datetime.now()
        
        # 重定向标准输出到日志
        import io
        old_stdout = sys.stdout
        string_io = io.StringIO()
        sys.stdout = string_io
        
        error_occurred = False
        error_info = ""
        
        try:
            messages = self.sandbox.run_script(script_path, params)
        except Exception as e:
            error_occurred = True
            error_info = str(e)
            traceback.print_exc()
            messages = None
            
            # 捕获所有脚本错误并记录到控制台输出
            error_trace = traceback.format_exc()
            logger.error(f"脚本执行错误: {str(e)}\n{error_trace}")
        
        # 获取控制台输出和错误信息
        try:
            console_output = string_io.getvalue()
        except AttributeError:
            # 如果getvalue()方法不存在，尝试其他方式获取输出
            console_output = ""
        finally:
            sys.stdout = old_stdout
        
        end_time = datetime.now()
        duration = (end_time - start_time).total_seconds()
        
        # 日志内容优化
        log_type = 'script_execution'
        script_success = False
        log_content = ""
        # 1. run_script 返回异常 dict
        if isinstance(messages, dict) and 'error' in messages:
            error_occurred = True
            error_info = messages.get('error', '')
            error_trace = messages.get('traceback', '')
            log_content = f"脚本执行异常: {error_info}\n{error_trace}\n{console_output}"
        # 2. main 返回 None 或空列表 - 修改为成功状态
        elif messages is None or (isinstance(messages, list) and len(messages) == 0):
            error_occurred = False  # 改为False，表示脚本执行成功
            script_success = True   # 改为True，表示脚本执行成功
            log_content = f"脚本执行完成，无消息返回\n{console_output}"
        # 3. 正常返回
        elif isinstance(messages, list):
            script_success = True
            log_content = console_output if console_output else "脚本执行完成"
        else:
            error_occurred = True
            error_info = "main 返回值类型错误"
            log_content = f"main 返回值类型错误\n{console_output}"

        # 再次验证任务是否存在（防止任务在执行过程中被删除）
        if task and task.id:
            try:
                with app.app_context():
                    # 创建脚本执行日志记录
                    script_execution_log = TaskExecutionLog(
                        task_id=task.id,
                        start_time=start_time,
                        end_time=end_time,
                        duration=duration,
                        status='成功' if script_success and not error_occurred else '失败',
                        log_content=log_content,
                        log_type='script_execution'
                    )
                    db.session.add(script_execution_log)
                    # 不再更新task.last_run_status、last_run_time、last_run_duration
                    db.session.commit()
            except Exception as e:
                logger.error(f"保存日志记录失败: {str(e)}")
        else:
            logger.warning(f"任务不存在，跳过日志创建: script_path={script_path}, task_id={task_id}")
        
        # 返回消息列表或错误信息
        if script_success and isinstance(messages, list):
            return messages
        else:
            return messages  # 直接返回 error dict 或 None

    def send_message_to_robot(self, messages: List[str], webhook: str, secret: str = None, task_id: int = None, at_persons: str = '') -> bool:
        """
        向指定机器人发送消息列表
        :param messages: 消息列表
        :param webhook: 钉钉机器人的webhook地址
        :param secret: 钉钉机器人的签名密钥
        :param task_id: 任务ID
        :param at_persons: @人的手机号列表，多个手机号用逗号分隔
        :return: 是否发送成功
        """
        if not messages or not isinstance(messages, list):
            logger.error("消息列表为空或格式错误")
            return False
            
        try:
            # 使用executor service的数据库配置
            import os
            import sys
            sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
            from models import ScheduledTask, TaskExecutionLog
            from services.executor_service import app, db
            
            # 获取任务信息
            if task_id:
                with app.app_context():
                    task = ScheduledTask.query.get(task_id)
            else:
                task = None
                
            start_time = datetime.now()
            
            # 发送所有消息
            success_count = 0
            total_count = len(messages)
            error_messages = []
            
            for i, msg in enumerate(messages):
                if isinstance(msg, str):
                    success, error_msg = self._send_single_message(webhook, secret, msg, at_persons)
                    if success:
                        success_count += 1
                    else:
                        error_messages.append(f"第{i+1}条消息发送失败: {error_msg}")
            
            end_time = datetime.now()
            duration = (end_time - start_time).total_seconds()
            
            # 创建钉钉消息发送日志记录
            if task and task.id:
                dingtalk_log_content = f"钉钉消息发送详情:\n"
                if at_persons:
                    dingtalk_log_content += f"@人配置: {at_persons}\n"
                for i, msg in enumerate(messages, 1):
                    if isinstance(msg, str):
                        dingtalk_log_content += f"第{i}条消息: {msg[:100]}{'...' if len(msg) > 100 else ''}\n"
                
                if error_messages:
                    dingtalk_log_content += f"\n发送失败的消息:\n" + "\n".join(error_messages)
                else:
                    dingtalk_log_content += "\n所有消息发送成功"
                    
                dingtalk_execution_log = TaskExecutionLog(
                    task_id=task.id,
                    start_time=start_time,
                    end_time=end_time,
                    duration=duration,
                    status='成功' if success_count == total_count else '失败',
                    log_content=dingtalk_log_content,
                    log_type='dingtalk_log'
                )
                db.session.add(dingtalk_execution_log)
                db.session.commit()
            else:
                logger.warning(f"任务不存在，跳过钉钉消息发送日志创建: task_id={task_id}")
            
            return success_count == total_count
            
        except Exception as e:
            logger.error(f"发送消息到机器人失败: {str(e)}")
            return False

    def _send_single_message(self, webhook: str, secret: str, message: str, at_persons: str = '') -> Tuple[bool, str]:
        """
        发送单条钉钉消息
        :param webhook: 钉钉机器人的webhook地址
        :param secret: 钉钉机器人的签名密钥
        :param message: 要发送的消息内容
        :param at_persons: @人的手机号列表，多个手机号用逗号分隔
        :return: (是否成功, 错误信息)
        """
        try:
            if secret:
                timestamp = str(round(time.time() * 1000))
                secret_enc = secret.encode('utf-8')
                string_to_sign = f"{timestamp}\n{secret}"
                string_to_sign_enc = string_to_sign.encode('utf-8')
                hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
                sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
                
                url = f"{webhook}&timestamp={timestamp}&sign={sign}"
                logger.info(f"构建的URL: {url}")
            else:
                url = webhook

            headers = {'Content-Type': 'application/json'}
            
            # 构建消息数据
            data = {
                "msgtype": "text",
                "text": {"content": message}
            }
            
            # 如果有@人配置，添加@功能
            if at_persons and at_persons.strip():
                # 解析手机号列表
                phone_numbers = [phone.strip() for phone in at_persons.split(',') if phone.strip()]
                if phone_numbers:
                    data["at"] = {
                        "atMobiles": phone_numbers,
                        "isAtAll": False
                    }
                    logger.info(f"添加@人配置: {phone_numbers}")
            
            logger.info(f"发送的数据: {data}")
            
            response = requests.post(url, headers=headers, json=data, timeout=10)
            logger.info(f"响应状态码: {response.status_code}")
            logger.info(f"响应内容: {response.text}")
            
            if response.status_code != 200 or response.json().get('errcode') != 0:
                error_msg = f"钉钉API返回错误: {response.text}"
                logger.error(error_msg)
                return False, error_msg
            return True, ""
        except Exception as e:
            error_msg = f"钉钉消息发送异常: {str(e)}"
            logger.error(error_msg)
            logger.error(f"异常详情: {traceback.format_exc()}")
            return False, error_msg

    def send_dingtalk_message(self, webhook: str, secret: str = None, message: str = None) -> bool:
        """
        发送钉钉消息（向后兼容的方法）
        :param webhook: 钉钉机器人的webhook地址
        :param secret: 钉钉机器人的签名密钥
        :param message: 要发送的消息内容
        :return: 是否发送成功
        """
        if not message:
            return False
        success, _ = self._send_single_message(webhook, secret, message)
        return success

if __name__ == '__main__':
    # 示例用法
    engine = TaskEngine()
    script_path = 'user_script.py'  # 替换为实际的脚本路径
    params = {
        'username': 'test_user',
        'count': 5
    }
    result = engine.execute_task(script_path, params)
    print(f"任务执行结果: {result}")